﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Cloo;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Devices;
using RayDen.RayEngine.Core.Types;

namespace RayDen.RayEngine.IntersectionEngines.OpenCL {

    public class CustomClIntersectionDevice : IntersectionEngine {


        protected ClDeviceContext clContext;
        protected Stopwatch wallclock;
        protected double statsDeviceIdleTime;
        protected double statsDeviceTotalTime;
        protected Task renderingTask;
        protected CancellationTokenSource cancelToken;
        protected ConcurrentQueue<RayBuffer> todoRayBuffers, doneRayBuffers;
        //Buffers

        protected ComputeBuffer<Point> verts;
        protected ComputeBuffer<TriangleInfo> tris;
        protected ComputeBuffer<GpuLBVHNode> tree;
        protected ComputeBuffer<RayData> rays;
        protected ComputeBuffer<RayHit> rayHits;



        public CustomClIntersectionDevice(RayEngineScene scene, bool lowLatency, int index)
            : base(scene) {
                wallclock = new Stopwatch();
                this.todoRayBuffers = new ConcurrentQueue<RayBuffer>();
                this.doneRayBuffers = new ConcurrentQueue<RayBuffer>();
                this.started = false;
                var device = ComputePlatform.Platforms[0].Devices[0];
                clContext = new ClDeviceContext() { KernelSrc = Kernels.MP_PbrtBVHKernel };
                clContext.Initialize();
                clContext.SetupDevice("Intersect");

                var rayBufferSize = lowLatency ? (RayBuffer.RayBufferSize / 8) : RayBuffer.RayBufferSize;
                var sceneVertices = scene.Vertices.ToArray();
                var sceneTriangles = scene.Triangles.ToArray();
                Tracer.TraceLine("Vertices Data Size {0:F3} MBytes", (sceneVertices.Length * 12f) / (1024f * 1024f));
                Tracer.TraceLine("Indexes Data Size {0:F3} MBytes", (sceneTriangles.Length * 12f) / (1024f * 1024f));
                var da = new BvhDataAdapter(scene);
                var st = DateTime.UtcNow;
                TriangleDataInfo[] triData = null;
                var treeData = da.BuildTDData(out triData);
                //scene.Triangles = triData.ToList();
                var dc = treeData.Count(item => item.IsLeaf);
                Tracer.TraceLine("Bvh Leaf nodes {0}", dc);
                verts = new ComputeBuffer<Point>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, sceneVertices);
                tris = new ComputeBuffer<TriangleInfo>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, triData.Select(item=>item.GetInfo()).ToArray());


                Tracer.TraceLine("BVH Data Size {0:F3} MBytes", (treeData.Length * 32f) / (1024f * 1024f));
                tree = new ComputeBuffer<GpuLBVHNode>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, treeData);
                rays = new ComputeBuffer<RayData>(clContext.context, ComputeMemoryFlags.ReadOnly, rayBufferSize);
                rayHits = new ComputeBuffer<RayHit>(clContext.context, ComputeMemoryFlags.ReadWrite, rayBufferSize);
                Tracer.TraceLine("Bvh Build and Load Time {0}", DateTime.UtcNow - st);

                clContext.kernel.SetMemoryArgument(0, rays);
                clContext.kernel.SetMemoryArgument(1, rayHits);
                clContext.kernel.SetMemoryArgument(2, verts);
                clContext.kernel.SetMemoryArgument(3, tris);
                clContext.kernel.SetValueArgument(4, (uint)tris.Count);
                clContext.kernel.SetValueArgument(5, (uint)tree.Count);
                clContext.kernel.SetMemoryArgument(6, tree);
        }

        #region IIntersectionEngine Members

        public override void Stop() {
            wallclock.Stop();
            started = false;

            if (renderingTask.Status == TaskStatus.Running) {
                cancelToken.Cancel();
                renderingTask.Dispose();
                renderingTask = null;
            }

            this.todoRayBuffers = new ConcurrentQueue<RayBuffer>();
            this.doneRayBuffers = new ConcurrentQueue<RayBuffer>();
        }

        public void Interrupt() {
            wallclock.Stop();
            if (this.renderingTask.Status == TaskStatus.Running) {
                cancelToken.Cancel();
            }
        }

        public override void Start() {
            if (started)
                return;
            started = true;
            wallclock.Start();
            statsDeviceIdleTime = 0.0;
            statsDeviceTotalTime = 0.0;
            cancelToken = new CancellationTokenSource();
            this.renderingTask = Task.Factory.StartNew(() => RayIntersection(cancelToken.Token, this), cancelToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        public override void Trace(RayBuffer rayBuffer) {
#if DEBUG
            try
            {
#endif
                var device = this;
                var raysArg = rayBuffer.RaysInfo;
                GCHandle raysHandle = GCHandle.Alloc(raysArg, GCHandleType.Pinned);
                clContext.kernel.SetValueArgument(7, rayBuffer.GetRayCount());
                device.clContext.commands.Write(device.rays, true, 0, rayBuffer.Size, raysHandle.AddrOfPinnedObject(),
                                                null);
                device.clContext.commands.Execute(device.clContext.kernel, null, new long[] {rayBuffer.RaysInfo.Length},
                                                  new long[] {32}, null);
                //device.clContext.commands.Finish();
                GCHandle arrCHandle = GCHandle.Alloc(rayBuffer.rayHits, GCHandleType.Pinned);
                device.clContext.commands.Read(device.rayHits, false, 0, rayBuffer.Size, arrCHandle.AddrOfPinnedObject(),
                                               null);
                //device.clContext.commands.Finish();
                rayBuffer.traced = true;
                arrCHandle.Free();
                raysHandle.Free();
#if DEBUG

            }
            catch (Exception ex)
            {
                Tracer.TraceLine("Exception in Trace Method {0}", ex.Message);
                Tracer.TraceLine("{0}", ex.StackTrace);
            }
#endif

        }

        protected static void RayIntersection(CancellationToken ct, CustomClIntersectionDevice device) {
            Tracer.TraceLine("Ray-triangle intersection device {0}", device.GetType().Name);
            while (!ct.IsCancellationRequested) {
                //var t1 = 0;// device.wallclock.ElapsedTicks;
                RayBuffer rayBuffer;

#if OLD 
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer))
                    continue;

#else
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer)) {
                    continue;
                }
#endif

                //var t2 = DateTime.UtcNow.Ticks;
                device.Trace(rayBuffer);
                //Tracer.TraceLine("Result returns {0} items ", buffer.rayHits.Length);
                //            Tracer.TraceLine("Intersections {0} items ", buffer.rayHits.Count(p => p.index != uint.MaxValue));
                //var t3 = device.wallclock.ElapsedTicks;

                //device.statsDeviceIdleTime += t2 - t1;
                //device.statsDeviceTotalTime += t3 - t1;
                device.statsTotalRayCount += rayBuffer.RaysInfo.Length;
                device.doneRayBuffers.Enqueue(rayBuffer);
            }
        }


        public override void PushRayBuffer(RayBuffer rayBuffer,  int clientIdx) {
            todoRayBuffers.Enqueue(rayBuffer);
        }

        public override RayBuffer PopRayBuffer( int info) {
            RayBuffer result;
            doneRayBuffers.TryDequeue(out result);
            return result;
        }

        #endregion

        #region IDisposable Members

        public override void Dispose() {

            if (clContext.context != null) {
                
                foreach (var buffer in new ComputeMemory[] { rayHits, rays, tree, tris, verts}) {
                    buffer.Dispose();
                }

                clContext.commands.Dispose();
                clContext.program.Dispose();
                clContext.kernel.Dispose();
                clContext.context.Dispose();
            }

        }

        #endregion
    }
}
